########################################################################
#  Pyfm - personal finance data-base
#  Copyright (C) 2007,8 Ehud Ben-Reuven
#  udi@benreuven.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation version 2.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
########################################################################
""" Definition of the syntax of values assigned to tags.
Tags are used in flat-line tags and also as object attributes.
If you enter flat-lines manually you have to follow the instructions given for each tag in this file.
Each tag is defined as a class in this file. The class name should be identical to the tag name.
Tags that don't appear in this module are assumed to be a Python string:
class XXX(str): pass

Most of the tags are specified by the OFX standard, visit www.ofx.net  for more information on syntax.
However, in some cases a  more flexible syntax is allowed.
In some cases the usage of the tags is different from the standard.
In some cases several OFX tags are represented by just one tag.

Date-Time tags (tags that begin with DT) is such  a big issue that it has its own dedicated module (obj.dt)
read it to follow the different ways in which dates can be entered or displayed.

The class definition of a tag should meet some requirments,
these are automatically meet if you inherite a Python type and dont override its methods.
The requirments are:
* The __init__ method should accept string as a parameter:
When a flat-line text file is loaded to memory (by obj.flat.parse), every tag-value pair is converted into an object of the class.
The object is created with the value string taken from the flat-file as the initalization parameter.
* It should be possible to compare objects from same class.
* Tags that appear in a key of an object defined in the model must have a hash method.
If you defined comparison methods (__cmp__, __eq__,...) you may need to also specificy an appropriate __hash__ method
even if the super-class had its own hash.
* __str__, __repr__
* Specifice model tools may require additional methods. For example: ASSETCLASS.ischecking
"""
# Tags related to time
#==============
from obj.dt import DT
import lib.secid
import re

class DTASOF(DT): pass # also stands for OFX's DTYIELDASOF
class DTSETTLE(DT): pass # also stands for OFX's DTPOSTED
class DTTRADE(DT): pass 
# TODO add usage of DTAVAIL
class DTMAT(DT): pass # Maturity date
class DTSTART(DT): pass
class DTEND(DT): pass
#class DTRS(DTSTART): # Report start date
    #def __init__(self,s):
        #"""Round report start to the start of the day"""
        #super(DTRS,self).__init__(s)
        #self.startofday()
#class DTRE(DTEND): # Report end of date
    #def __init__(self,s):
        #"""Round report end to the end of the day"""
        #super(DTRE,self).__init__(s)
        #self.endofday()
class DTCOUPON(DT): pass

################################
class BOOL(int):
    def __new__(cls,s=0): # int is imutable
        if isinstance(s,str):
            if s!='TRUE' and s!='FALSE': raise Exception('Illegal value')
            return super(BOOL,cls).__new__(cls,s=='TRUE')
        else:
            return super(BOOL,cls).__new__(cls,bool(s))                     
    def __repr__(self): return "TRUE" if self else "FALSE"
    def __str__(self): return `self`
class COMPLETE(BOOL): pass # Used in AccountSource, if true then all non-zero holdings of the account receive a statement in source. 
class FULL(BOOL): pass # Used in AccountSource or HoldingSource, if true then for all relevant holdings, all transactions between DTSTART/END appear in source.
class FIX(BOOL): pass # A command-line parameter
class _SKIP_(BOOL): pass # An object that should be ignored.

class AIDX(int):
    def __str__(self): return "aidx"+super(AIDX,self).__str__()
class HIDX(int):
    def __str__(self): return "hidx"+super(HIDX,self).__str__()

class FLOAT(float):
    def __repr__(self): return "%g"%float(self)
    def __str__(self): return "%.4f"%float(self)

class UNITS(FLOAT):
    def __str__(self): return super(FLOAT,self).__str__()+"*"

class PERCENT(FLOAT): #value is already in perecent units
    def __str__(self): return super(YIELD,self).__str__()+"%"
class YIELD(PERCENT): pass # Percent yield of a bond

class CURRENCY(str):
    def __new__(cls,s='USD'): # str is imutable
        if not lib.secid.validCurrency(s):
            raise Exception('Illegal value')
        return super(CURRENCY,cls).__new__(cls,s)
    def __str__(self):
        if self=='USD': return '$'
        return super(CURRENCY,self).__str__()
class ORIGCURRENCY(CURRENCY):
    pass
class CURDEF(CURRENCY):
    pass
class _CURRENCY_(CURRENCY):
    pass

# TAGS related to Money
#===============
# TODO subclass MONEY from Obj (see TODO in Obj) and rename it to Money and add
# to it currency attribute
class MONEY(FLOAT): pass
# TODO in the following sub-class instead sub-class from FLOAT and generate for each class
# a "shadow" sub-class Money (e.g. Fees, Commission, ...) all other Objects should use this shadow sub-classes
# instead of the following tags that don't carry a CURRENCY (or _CURRENCY_) with them.
class FEES(MONEY):
    def __str__(self): return "-"+super(FEES,self).__str__()
class COMMISSION(MONEY): pass
class LOAD(FEES): pass
class INTEREST(MONEY):
    """extension to OFX:
    money distributed by a holding in a dividend or interest transaction
    should be same as TOTAL
    """
    def __str__(self): return "+"+super(INTEREST,self).__str__()
class ACCRDINT(INTEREST): pass
class ACCRDINTUSD(ACCRDINT): pass
class TAXES(FEES): pass
class WITHHOLDING(TAXES): pass
class STATEWITHHOLDING(TAXES): pass

class TOTAL(MONEY): # also stands for OFX's TRNAMT
    def __str__(self): return "="+super(TOTAL,self).__str__()
class MKTVAL(TOTAL): pass
class COST(TOTAL): pass
class ACCRDMKTVAL(MKTVAL):
    """Market value including accrued interest"""
    pass
class MKTVALUSD(MKTVAL): pass
class UNITPRICE(MONEY):
    """The value of one unit of an Asset.
The value is expressed by CURRENCY that appear in the same line as UNITPRICE and if the line
has no such tag then the CURRENCY of the Asset in the line should be used
"""
    def __str__(self): return "*"+super(UNITPRICE,self).__str__()
class FIXEDPRICE(UNITPRICE): pass # Unit price of asset with fixed price, in Orginal currency
class MARKUP(UNITPRICE): pass
class MARKDOWN(UNITPRICE): pass

class NLINE(str):
    # Has the structure <File-name>#<line-number>
    pass
class OLINE(NLINE):
    pass
class COMPONENT(str):
    """A string that describes one of the components of an asset and percentge.
It has the form:
<component>=<percent>
where the component can be broken into a hierarchy using dots.
For example:
stock.large=21.2
cash=0
bond.municipal=100
Several COMPONENT can be collected togther to describe a single composed Asset
using the Composition Class
"""
    reo=re.compile("(?P<component>[A-Za-z\_\.]+)\=(?P<percent>[0-9]+(\.[0-9]*)?)\Z")
    @classmethod
    def _explode(cls,s):
        r=cls.reo.match(s)
        return r.group('component'),r.group('percent')          
    def explode(self):
        component,percent=self._explode(self)
        percent=float(percent)
        return component,percent

class ID(str):
    """A string that is used as an identifier. It may be prefixed by a domain and a sub-domain."""
    reo=re.compile("((?P<domain>[A-Z]+)(=(?P<subdomain>[^=]+))?=)?(?P<label>[^\s]+)\Z")
    @classmethod
    def _explode(cls,s):
        r=cls.reo.match(s)
        if not r: return None,None,s
        return r.group('domain'),r.group('subdomain'),r.group('label')          
    def explode(self):
        return self._explode(self)
    
class FITID(ID):
    """Financial Institute Transaction ID.
Each transaction has its own unique ID. However, in some cases a single transaction can have more than one ID
if they have different IDs."""
    
class OFITID(FITID): pass

currenttypes=['CHECKING','SAVINGS','MNYMARKET','CREDITLINE','CREDITCRD',
          'CASH', # FIXME CASH is asset of type CHECKING used in investment account, why is it used as an ACCTTYPE?
          'MORTGAGE'] # When ACCTYPE stands for LOANACCTTYPE

class SECID(ID):
    """SECID a tag that can uniquely identify an asset.
SECID tag must have the following syntax:
<SECID> = <UNIQUEIDTYPE> '=' <UNIQUEID>
<UNIQUEIDTYPE> = <domain> | <domain> '=' <sub-domain>
<UNIQUEID> = <label>
Possible domains are:
    *  CUSIP - Identifier US assets
    * ISIN - World wide standard (super set of CUSIP) usually the first two characters identify the country
    * CURRENT - Current Cash account:
    * <label> = <CURRENCY>
    * <sub-domain> in currenttype
    * TIKCER - Ticker that is recognized by finance.yahoo.com
    * BROKERID, ORG - ID recognized by the bank (the FIID tag in OFX)
    * TASE - label is a number in Tel-Aviv stock exchange.
    * SWX - label is a number used by Swiss stock exchange
    * X - label is a per of two currencies. For example:
    * X=USDILS is one unit of USD but the CURRENCY used to represent its value is ILS. changing to USDILS=X gives a valid ticker. You should multiple by the value of this asset if you want to convert something to ILS, however, in some cases it is better to retrieve this exchange rates from Bank-of-Israel and not yahoo.
    * X=ILSUSD is one unit of ILS in USD currency.
    * X=EURUSD is one unit of EURO but represented in USD Currency. EURUSD=X is also a valid yahoo ticker.
    * OTHER - sub-domain must be specified but it and the label can be anything.
        
    """
    def getcusip(self):
        if self.startswith('CUSIP='): return self[6:]
        if self.startswith('ISIN=US'): return self[7:16]
        
    def __new__(cls,s): # str is imutable
        domain,subdomain,label=cls._explode(s)
        if domain=='CUSIP':
            assert not subdomain
            if label.startswith('#'): label=label[1:]
            if len(label)==12 and label.endswith('000'): label=label[:-3]
            label=label.upper()
            if not lib.secid.validCusip(label):
                print 'Inavlid CUSIP ',s
            else:
                s='ISIN='+lib.secid.makeIsin('US',label) # Convert valid CUSIP to ISIN
        elif domain=='ISIN':
            assert not subdomain
            label=label.upper()
            if len(label)==11:
                label=lib.secid.makeIsin(label[:2],label[2:],True)
            if not lib.secid.validIsin(label):
                print 'Inavlid ISIN ',s
            else:
                s='ISIN='+label
        elif domain=='CURRENT':
            if subdomain=='MONEYMRKT':
                subdomain='MNYMARKET'
            if subdomain not in currenttypes:
                print 'Invalid Current account ',s
            if not lib.secid.validCurrency(label):
                print 'Invalid Currency ',s
            s='CURRENT='+subdomain+'='+label
        elif domain in currenttypes: # ,'MARGIN','SHORT']:
            if not lib.secid.validCurrency(label):
                print 'Invalid Currency ',s
            s='CURRENT='+domain+'='+label
        #elif domain=='TICKER':
            #if not lib.secid.validTicker(label):
                #print 'Invalid ticker ',s
        elif domain in ['BROKERID', 'ORG']:
            assert subdomain
        elif domain=='TASE':
            if label.endswith('.0'): label=label[:-2]
            if not label.isdigit():
                print 'Invalid TASE #',s
            else:
                s='TASE='+label
        elif domain=='SWX': # Swiss Exchange
            pass
        elif domain=='OTHER':
            if not subdomain:
                print 'Subdomain is missing',s
        elif domain=='X':
            if not lib.secid.validCurrency(label[:3]) or not lib.secid.validCurrency(label[3:]):
                print 'Not a valid pair of currencies',s
        else:
            raise Exception('Unknown/missing SECID domain '+s)
        return super(SECID,cls).__new__(cls,s)

class STR(str):
    legal_values=[]
    lookup={}
    def __new__(cls,s=''): # str is imutable
        if not cls.legal_values:
            if s in cls.lookup:
                s=cls.lookup[s]          
        elif s not in cls.legal_values:
            s=s.upper()
            if s not in cls.legal_values+['OTHER']:
                if s in cls.lookup:
                    s=cls.lookup[s]
                else:
                    print 'Unknown',cls.__name__,'type',s,'Switching to OTHER'
                    s='OTHER'
        return super(STR,cls).__new__(cls,s)

class LSTR(str):
    """Lower case string"""
    def __new__(cls,s=''): # str is imutable
        s=s.lower()
        return super(LSTR,cls).__new__(cls,s)

class PAYEEID(str):
    """Identifing who received the money in a DEBIT transaction in a CHECKING account"""
    pass

class COUPONFREQ(STR):
    """How many times a year do you receive a dividend (coupon).
    The period can be followed by a day-month value.
    For example: Q:20-Mar will have a dividend at 20-Mar, 20-Jun, 20-Sep, 20-Dec"""
    legal_values=['MONTHLY', 'QUARTERLY', 'SEMIANNUAL', 'ANNUAL',
        'MATURITY'] # None OFX, the dividend will be given at the maturity of the asset
    lookup = {'M':'MONTHLY', 'Q':'QUARTERLY', 'H':'SEMIANNUAL', 'Y':'ANNUAL','ANNUALLY':'ANNUAL'}
    def __new__(cls,s=''): # str is imutable
        ss=s.split(':')
        s=ss[0]
        if s not in cls.legal_values:
            s=s.upper()
            if s not in cls.legal_values+['OTHER']:
                if s in cls.lookup:
                    s=cls.lookup[s]
                else:
                    print 'Unknown',cls.__name__,'type',s,'Switching to OTHER'
                    s='OTHER'
        if len(ss)>1: s+=':'+ss[1]
        return super(STR,cls).__new__(cls,s)

class COUNTRY(STR):
    "Country related to an asset"
    lookup = {'USA':'US', 'NE':'NL', 'Neterland':'NL', 'Lux':'LUX',
              'New Zealand':'NZ',
              'EUR':'EU',
              'France':'FR', 'GB':'UK', 'Germany':'DE', 'GR':'DE',
              'Switzerland':'CH', 'SWITZERLAND':'CH',
              'Canada':'CA',
              'Finland':'FI', 'FL':'FI', }
    

class ACCTTYPE(STR):
    legal_values = list(currenttypes)
    legal_values += ['INVESTMENT','RETIREMENT',
                'MARGIN', 'SHORT', 'OTHER'] # When ACCTTYPE stands for SUBACCTFUND
    lookup={'MONEYMRKT':'MNYMARKET'}
    def iscurrent(self):
        return self in currenttypes

class TRNTYPE(STR):
    """Combination of the TRNTYPE and INCOMETYPE in OFX with few extensions."""
    legal_values=['CREDIT','DEBIT','OPENING_BALANCE','DIV','INT','FEES',
            'BUY','SELL', # Extension to OFX
            'CGLONG','CGSHORT', # Distribution of money made by a US Mutual-fund that is classified to either long or short. (It's common for the classification to be changed at 1Q in the following year.)
            'CG', # Distribution of money that is classified as Capital Gain without knowing if it is long or short. (probably by non-US mutual fund.)
            'REVERSAL', # Undo of a previouse transaction, common for US mutual-funds
            'TAX',
            'TRANSFER', # From another account or when one asset was switched to anoter without generating a tax event
            'MISC','CHECK','IN','FEE','IN','OUT','PAYMENT','ATM','CASH','SRVCHG','XFER','EXPENSE'
            ]
    lookup={'DIVIDEND':'DIV','INTEREST':'INT',
        'LG CAP GAIN':'CGLONG','SH CAP GAIN':'CGSHORT',
        'LINKAGE GAIN':'CG', # Gain generated in a deposit as a result of a change in an underling index
        'CAPITAL GAIN':'CG',
        'CGAIN':'CG',
        }

class ASSETCLASS(STR):
    # The zoo of all kinds of Assets, there may be overlaps between the classification so be careful not to count the same asset
    # twice when you want to have a break-down of your portfolio which is based on asset class.
    legal_values=[
        'INDEX', # An asset that is only used as a place holder for a market index (see ETF for a real asset that tracks an INDEX)
        'DOMESTICBOND', 'INTLBOND', 'LARGESTOCK', 'SMALLSTOCK', 'INTLSTOCK', 'MONEYMRKT', # From OFX
        'BOND','STOCK','CHECKING','CREDITCRD','FIXEDPRICE',
        'OPT',
        # Deposits
        'DEPOSIT','LINKED DEPOSIT','RANGE NOTE','STRUCTURE',
        # Mutual Funds
        'MF', # Mutual Fund
        'ETF', # Mutual Fund that tracks an Index
        'VALUE','LARGE','BLEND','GROWTH','MEDIUM','SMALL','HEALTH-CARE','REAL-ESTATE','MUNICIPAL', # Sector Mutual Funds
        # Alternative Investments
        'HF', # Hedge Fund
        'PE', # Private Equity
        'K1', # Alternative investment which at the end of the year hands-out its own IRS tax report (K-1 which is different from the 1099 report given by the bank)
        # Others
        'MORTGAGE','LIFE INSURANCE', # Things that you don't always think of as assets (but they are!)
        'KUPAT GEMEL','KEREN PENSYA','KEREN HISHTALMUT', # Israeli tax benefit plans
    ]
    lookup={'CURRENT':'CHECKING',
        'CASH':'CHECKING', # CASH is the name used in Investment accounts.
        'DEBT':'BOND','MUTUAL FUND':'MF',
        'ASSET_GROUP':'HF','HEDGE FUNDS':'HF',
        'ASSET_GROUP=PRIVATE EQUITY':'PE','PRIVATE EQUITY':'PE',
        'LINKED TO USD DEPOSIT':'LINKED DEPOSIT',
        'SAVING PLAN':'DEPOSIT','FC DEPOSIT':'DEPOSIT',
        'ASSET_GROUP=CASH':'CHECKING',
        'MONEY MARKET':'MONEYMRKT',
        'ASSET_GROUP=FIXED INCOME/MUNICIPAL':'MUNICIPAL',
        'ASSET_GROUP=HEDGE FUNDS':'HF','HEDGE FUND':'HF','FUND OF HEDGE FUNDS':'HF',
        'TAX=K1':'K1',
        'ASSET_GROUP=FIXED INCOME':'BOND',
        'ASSET_GROUP=CAPITAL GUARANTEE':'STRUCTURE','CAPITAL GUARANTEE':'STRUCTURE',
        'FIXED INCOME':'BOND',
        
    }
    def ischecking(self):
        return self=='CHECKING' or self=='CREDITCRD'
    def isfixedprice(self):
        return self.ischecking() or self in ['FIXEDPRICE','MONEYMARKET','DEPOSIT']

class CHECKNUM(int): pass

class RELFITID(FITID): pass
class RELSECID(SECID): pass

class ACCTID(STR):
    pass

class SECNAME(STR):
    pass

class XACCTID(ACCTID): pass
class XSECNAME(SECNAME): pass
class XUNITS(UNITS): pass
class XTRNTYPE(TRNTYPE): pass
